refactor(chat)!: namespace exports for React Server Component support - #53
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the primitives usable from React Server Components, by switching from
Object.assigncompounds to Base UI-style namespace exports.The problem
Every compound sub-component resolved to
undefinedin a server component:A server component importing a
"use client"module receives a proxy of that module's named exports. It cannot read properties off an exported value — andObject.assign(ComposerRoot, { Container })is exactly that. Bare<AskUser />worked;<AskUser.Header />did not.The shape
Base UI's structure, verified against their published output: the barrel and the parts layer carry no directive, and it lives one level down on the component module.
Unlike Base UI we don't need a file per part — the boundary only has to sit below the barrel — so each component stays in one file and the single-file convention survives.
I proved the mechanism in an isolated spike before touching anything, with both shapes in one Next app:
export * as Widget from './widget.parts.js'Object.assign(WidgetRoot, { Label })Element type is invalid… got: undefinedBreaking change
<Composer>→<Composer.Root>, and likewise forMessage,Thread,Steps,Reasoning,Chip,Attachments,AskUser. Sub-component names, hooks and type names are unchanged, so migration is mechanical. Changeset included.The build had to change too
A bundle carries one top-level directive, so bundling collapses the per-part boundaries this depends on. The package now ships unbundled — one module per source file, emitted by tsc instead of bunup.
That deleted two workarounds from #51 rather than carrying them forward:
dts: { inferTypes: true }is gone. tsc infers declarations natively; there is no isolated-declarations pass to eraseObject.assigncompounds tounknown.NODE_ENV=productionis gone. tsc always emits the productionreact/jsx-runtime, so the dev-runtime bug can't recur from an unset env var.One wrinkle worth recording. I first added
.jsextensions to thesrcimports, which is the conventional ESM-first approach — and Turbopack refused to resolve./actions.jsagainstactions.tsx. It has noextensionAliasequivalent, so that broke the app'stranspilePackagesdev loop, which deliberately compiles package source with no build step. Source therefore stays extensionless andscripts/add-dist-extensions.mjsadds extensions to the emitted output instead (231 specifiers). Dev loop unchanged,distvalid ESM, publint clean.Tarball grew: 74.5 kB packed across 25 bundled files → ~102 kB across 115 unbundled files. Unpacked is slightly smaller since
internal/helpers are no longer duplicated per entry, but per-file gzip is less efficient. That's the price of correct client boundaries.Docs
AGENTS.mdnow describes both layers explicitly, because they legitimately differ: the app layer (components/ai/,components/ui/) keepsObject.assignand stays single-file copy-pasteable, while the package uses namespace exports. Rule 13 forbade barrel files, which the new pattern requires, so it carries an explicit exception. Both invariants that make this work — the barrels staying directive-free, and the build not bundling — are written down where someone would otherwise break them.Also updated: the package README's RSC section (its constraint is gone, replaced by a server-component transcript example),
CHAT_ARCHITECTURE.md, and thecontent/docs/primitives/*pages — including theirsource:frontmatter, which pointed at files that moved.COMPOSER.md,THREAD.md,MESSAGE.mdandDESIGN_SYSTEM.mddocument the app layer, so their<Message>examples are still correct and untouched.Verified
Against the packed tarball, installed into a Next 16 app with no
transpilePackages:Thread.Root/Message.Root/Message.Text/Chip.Root/Chip.Icon/Chip.Label/Reasoning.*/Steps.*/AskUser.*prerenders, with all 16 expecteddata-*part attributes in the HTMLuseComposer/useThread/useReasoningand event handlers builds and prerenderstranspilePackages) all green